//
//  BuzzDao.m
//  Buzz API Demo
//
//  Created by Peter Hopkins on 5/6/10.
//  Copyright 2010 Google Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

#import "BuzzDao.h"

#import "AclEntry.h"
#import "AclGroup.h"
#import "Author.h"
#import "ActivityStreamsConstants.h"
#import "BuzzAttachment.h"
#import "BuzzEntry.h"
#import "BuzzFeed.h"
#import "BuzzVisibilityElements.h"
#import "Comment.h"
#import "PhotoAttachment.h"
#import "PocoFeed.h"
#import "PocoEntry.h"
#import "PocoElements.h"
#import "Post.h"
#import "User.h"

#import "GDataEntryBase.h"
#import "GDataFeedBase.h"
#import "GDataOAuthAuthentication.h"
#import "GDataPerson.h"
#import "GDataServiceGoogle.h"
#import "GDataThreadingElements.h"

#import <CoreData/CoreData.h>

#define kBuzzURL @"https://www.googleapis.com/buzz/v1/"


@interface Callback : NSObject {
 @private
  id delegate_;
  SEL selector_;
}

@end

@implementation Callback

- (id)initWithDelegate:(id)delegate selector:(SEL)selector {
  if ((self = [super init])) {
    delegate_ = [delegate retain];
    selector_ = selector;
  }

  return self;
}


+ (Callback *)callbackWithDelegate:(id)delegate selector:(SEL)selector {
  return [[[Callback alloc] initWithDelegate:delegate selector:selector] autorelease];
}


- (void)callWithObjects:(id)firstObject, ... {
  NSMethodSignature *sig = [delegate_ methodSignatureForSelector:selector_];
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
  [invocation setSelector:selector_];

  va_list argumentList;

  if (firstObject) {
    [invocation setArgument:&firstObject atIndex:2];

    va_start(argumentList, firstObject);

    for (int i = 3; i < [sig numberOfArguments]; ++i) {
      id obj = va_arg(argumentList, id);
      [invocation setArgument:&obj atIndex:i];
    }

    va_end(argumentList);
  }

  [invocation invokeWithTarget:delegate_];
}

- (void)dealloc {
  [delegate_ release];

  [super dealloc];
}
@end


@interface BuzzDao (PrivateMethods)
- (void)save;

- (NSString *)applicationDocumentsDirectory;
- (void)startSync;
- (void)endSync;

- (void)ticket:(GDataServiceTicket *)ticket
finishedWithConsumptionFeed:(BuzzFeed *)feed
         error:(NSError *)error;
- (void)ticket:(GDataServiceTicket *)ticket
finishedWithCommentsFeed:(BuzzFeed *)feed
         error:(NSError *)error;
- (void)addTicket:(GDataServiceTicket *)ticket
       addedEntry:(BuzzEntry *)entry
            error:(NSError *)error;

- (NSManagedObject *)objectForValue:(NSString *)value
                         primaryKey:(NSString *)primaryKey
                             entity:(NSEntityDescription *)entity
     insertIntoManagedObjectContext:(NSManagedObjectContext *)context;
  
- (User *)user;
- (Author *)authorForPerson:(GDataPerson *)person;
- (Author *)authorForPocoEntry:(PocoEntry *)entry;
- (AclEntry *)aclEntryForAclEntryElement:(BuzzAclEntry *)element;
@end

@implementation BuzzDao

- (id)initWithDelegate:(id <BuzzDaoDelegate>)delegate {
  self = [super init];
  if (self) {
    buzzService_ = [[GDataServiceGoogle alloc] init];
    delegate_ = delegate;
  }
  return self;
}


- (BOOL)loadStore {
  NSManagedObjectModel *managedObjectModel =
      [NSManagedObjectModel mergedModelFromBundles:nil];    

  NSURL *storeUrl = [NSURL fileURLWithPath:
      [[self applicationDocumentsDirectory]
       stringByAppendingPathComponent: @"buzz.sqlite"]];

  store_ = [[NSPersistentStoreCoordinator alloc]
               initWithManagedObjectModel:managedObjectModel];

  NSError *error = nil;
  if (![store_
            addPersistentStoreWithType:NSSQLiteStoreType
            configuration:nil
            URL:storeUrl
            options:nil
            error:&error]) {
    [delegate_ dao:self storageError:error];
    [store_ release];
    return NO;
  }

  context_ = [[NSManagedObjectContext alloc] init];
  [context_ setPersistentStoreCoordinator:store_];
  
  return YES;
}


- (NSString *)applicationDocumentsDirectory {
  NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(
      NSDocumentDirectory, NSUserDomainMask, YES);
	return [searchPaths lastObject];
}



- (void)save {
  if ([context_ hasChanges]) {
    NSError *error = nil;
    if (![context_ save:&error]) {
      [delegate_ dao:self storageError:error];
    }
  }
}


- (void)setAuthorizer:(GDataOAuthAuthentication *)auth {
  [buzzService_ setAuthorizer:auth];
}


- (NSFetchedResultsController *)fetchConsumption:(id)delegate {
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"consumer = %@", [self user]];
  NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"updatedDate"
                                                          ascending:NO] autorelease]; 

  NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
  [request setEntity:[Post entityInManagedObjectContext:context_]];
  [request setPredicate:predicate];
  [request setSortDescriptors:[NSArray arrayWithObject:sort]];
  [request setFetchBatchSize:100];

  NSFetchedResultsController *controller =
      [[[NSFetchedResultsController alloc] initWithFetchRequest:request
                                           managedObjectContext:context_
                                             sectionNameKeyPath:nil
                                                      cacheName:nil] autorelease];
  [controller setDelegate:delegate];

  NSError *error = nil;
  [controller performFetch:&error];

  if (error) {
    [delegate dao:self storageError:error];
    controller = nil;
  }

  return controller;
}


- (NSFetchedResultsController *)fetchCommentsForPost:(Post *)post delegate:(id)delegate {
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"post = %@", post];
  NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"published"
                                                        ascending:YES] autorelease]; 
  
  NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
  [request setEntity:[Comment entityInManagedObjectContext:context_]];
  [request setPredicate:predicate];
  [request setSortDescriptors:[NSArray arrayWithObject:sort]];

  NSFetchedResultsController *controller =
  [[[NSFetchedResultsController alloc] initWithFetchRequest:request
                                       managedObjectContext:context_
                                         sectionNameKeyPath:nil
                                                  cacheName:nil] autorelease];
  [controller setDelegate:delegate];

  NSError *error = nil;
  [controller performFetch:&error];
  
  if (error) {
    [delegate dao:self storageError:error];
    controller = nil;
  }
  
  return controller;
}


- (NSFetchedResultsController *)fetchLikersForPost:(Post *)post delegate:(id)delegate {
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ANY likes = %@", post];
  NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"name"
                                                        ascending:YES] autorelease]; 

  NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
  [request setEntity:[Author entityInManagedObjectContext:context_]];
  [request setPredicate:predicate];
  [request setSortDescriptors:[NSArray arrayWithObject:sort]];
  
  NSFetchedResultsController *controller =
  [[[NSFetchedResultsController alloc] initWithFetchRequest:request
                                       managedObjectContext:context_
                                         sectionNameKeyPath:nil
                                                  cacheName:nil] autorelease];
  [controller setDelegate:delegate];
  
  NSError *error = nil;
  [controller performFetch:&error];
  
  if (error) {
    [delegate dao:self storageError:error];
    controller = nil;
  }
  
  return controller;
}


- (void)startSync {
  UIApplication* app = [UIApplication sharedApplication];
  [app setNetworkActivityIndicatorVisible:YES];

  syncCount_++;
}

- (void)endSync {
  syncCount_--;

  if (syncCount_ == 0) {
    UIApplication* app = [UIApplication sharedApplication];
    [app setNetworkActivityIndicatorVisible:NO];
  }  
}


- (void)syncConsumption {
  GDataOAuthAuthentication *auth = [buzzService_ authorizer];
  if (![auth canAuthorize]) {
    NSLog(@"Sync aborted due to lack of authentication");
    return;
  }

  [self startSync];

  NSURL *postsURL = [NSURL URLWithString:@"activities/@me/@consumption"
                           relativeToURL:[NSURL URLWithString:kBuzzURL]];

  GDataQuery *query = [GDataQuery queryWithFeedURL:postsURL];
  [query setMaxResults:50];

  [buzzService_ fetchFeedWithQuery:query
                         feedClass:[BuzzFeed class]
                          delegate:self
                 didFinishSelector:@selector(ticket:finishedWithConsumptionFeed:error:)];
}


- (void)syncComments:(Post *)post {
  GDataOAuthAuthentication *auth = [buzzService_ authorizer];
  if (![auth canAuthorize]) {
    NSLog(@"Sync aborted due to lack of authentication");
    return;
  }

  if (![post repliesCount] ||
      [[post repliesUpdatedDate] isEqualToDate:[post repliesLastFetchedDate]]) {
    return;
  }

  [self startSync];
  
  NSURL *commentsURL = [NSURL URLWithString:[post repliesURL]];
  GDataQuery *query = [GDataQuery queryWithFeedURL:commentsURL];
  [query setMaxResults:50];
  
  [buzzService_ setServiceUserData:post];
  [buzzService_ fetchFeedWithQuery:query
                         feedClass:[BuzzFeed class]
                          delegate:self
                 didFinishSelector:@selector(ticket:finishedWithCommentsFeed:error:)];
}


- (void)syncLikers:(Post *)post {
  GDataOAuthAuthentication *auth = [buzzService_ authorizer];
  if (![auth canAuthorize]) {
    NSLog(@"Sync aborted due to lack of authentication");
    return;
  }
  
  [self startSync];
  
  NSLog(@"Fetching likers: %@", [post likersURL]);
  NSURL *likersURL = [NSURL URLWithString:[post likersURL]];
  GDataQuery *query = [GDataQuery queryWithFeedURL:likersURL];
  
  [buzzService_ setServiceUserData:post];
  [buzzService_ fetchFeedWithQuery:query
                         feedClass:[PocoFeed class]
                          delegate:self
                 didFinishSelector:@selector(ticket:finishedWithLikersFeed:error:)];
}


- (void)post:(Post *)post delegate:(id)delegate selector:(SEL)selector {
  BuzzEntry *entry = [[[BuzzEntry alloc] init] autorelease];
  [entry setContent:[GDataTextConstruct textConstructWithString:[post contentHTML]]];

  Callback *callback = [Callback callbackWithDelegate:delegate selector:selector];

  NSURL *selfURL = [NSURL URLWithString:@"activities/@me/@self"
                          relativeToURL:[NSURL URLWithString:kBuzzURL]];
  [buzzService_ setServiceUserData:callback];
  [buzzService_ fetchEntryByInsertingEntry:entry
                                forFeedURL:selfURL
                                  delegate:self
                         didFinishSelector:@selector(addTicket:addedEntry:error:)];
}

- (void)comment:(Comment *)comment onPost:(Post *)post delegate:(id)delegate selector:(SEL)selector {
  BuzzEntry *entry = [[[BuzzEntry alloc] init] autorelease];
  [entry setContent:[GDataTextConstruct textConstructWithString:[comment contentHTML]]];
  
  Callback *callback = [Callback callbackWithDelegate:delegate selector:selector];
  
  NSURL *commentURL = [NSURL URLWithString:[post repliesURL]];
  [buzzService_ setServiceUserData:callback];
  [buzzService_ fetchEntryByInsertingEntry:entry
                                forFeedURL:commentURL
                                  delegate:self
                         didFinishSelector:@selector(addTicket:addedEntry:error:)];
}



- (void)ticket:(GDataServiceTicket *)ticket
finishedWithConsumptionFeed:(BuzzFeed *)feed
         error:(NSError *)error {
  [self endSync];

  if (error != nil) {
    [delegate_ dao:self networkError:error];
    return;
  }

  User *user = [self user];
  if (!user) {
    return;
  }

  NSEntityDescription *postEntity = [Post entityInManagedObjectContext:context_];

  NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[[feed entries] count]];

  NSArray *entries = [feed entries];
  for (int i = [entries count] - 1; i >= 0; --i) {
    BuzzEntry *entry = [entries objectAtIndex:i];
    Post *post = (Post *)[self objectForValue:[entry identifier]
                                   primaryKey:@"atomID"
                                       entity:postEntity
               insertIntoManagedObjectContext:context_];

    if (!post) {
      continue;
    }

    [objects addObject:post];

    Author *author = [self authorForPerson:[[entry authors] objectAtIndex:0]];

    if (![[[entry updatedDate] date] isEqualToDate:[post updatedDate]]) {
      [post setUpdatedDate:[[entry updatedDate] date]];
      [post setAlternateURL:[[entry linkWithRelAttributeValue:@"alternate"] href]];
      [post setContentHTML:[[entry content] stringValue]];
      [post setPublishedDate:[[entry publishedDate] date]];
      [post setTitle:[[entry title] stringValue]];

      NSArray *aclEntries = [[entry visibility] aclEntries];
      NSMutableSet *visibilitySet = [NSMutableSet setWithCapacity:[aclEntries count]];

      for (BuzzAclEntry *aclEntry in aclEntries) {
        [visibilitySet addObject:[self aclEntryForAclEntryElement:aclEntry]];
      }

      [post setVisibility:visibilitySet];

      NSArray *attachmentElements = [entry attachments];
      NSMutableSet *attachmentSet = [NSMutableSet setWithCapacity:[attachmentElements count]];
      for (BuzzAttachment *attachmentElement in attachmentElements) {
        if ([[attachmentElement objectType] isEqualToString:kActivityObjectTypePhoto]) {
          GDataLink *previewLink = [attachmentElement previewLink];
          if (!previewLink) {
            continue;
          }

          PhotoAttachment *attachment = [[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"PhotoAttachment"
                                                                                            inManagedObjectContext:context_]
                                                 insertIntoManagedObjectContext:context_];
          [attachment setPreviewURL:[previewLink href]];
          [attachment setPreviewWidth:[BuzzAttachment widthForPreviewLink:previewLink]];
          [attachment setPreviewHeight:[BuzzAttachment heightForPreviewLink:previewLink]];
          [attachmentSet addObject:attachment];
        }
      }

      [post setAttachments:attachmentSet];

      [post setAuthor:author];
      [post setConsumer:user];
    }
 
    GDataLink *repliesLink = [entry linkWithRelAttributeValue:@"replies" type:kGDataLinkTypeAtom];
    if (![[[GDataThreadingLink threadingUpdatedDateForLink:repliesLink] date]
          isEqualToDate:[post repliesUpdatedDate]]) {
      [post setRepliesCount:[GDataThreadingLink threadingCountForLink:repliesLink]];
      [post setRepliesUpdatedDate:[[GDataThreadingLink threadingUpdatedDateForLink:repliesLink] date]];
      [post setRepliesURL:[repliesLink href]];
    }

    GDataLink *likersLink = [entry likedLink];
    if (![post likersURL]) {
      [post setLikersURL:[likersLink href]];
    }

    if (![[post likersCount] isEqualToValue:[BuzzEntry buzzCountForLink:likersLink]]) {
      [post setLikersCount:[BuzzEntry buzzCountForLink:likersLink]];
    }
  }

  [context_ obtainPermanentIDsForObjects:objects error:&error];

  [self save];
}


- (void)ticket:(GDataServiceTicket *)ticket
finishedWithCommentsFeed:(BuzzFeed *)feed
         error:(NSError *)error {
  [self endSync];
  
  if (error != nil) {
    [delegate_ dao:self networkError:error];
    return;
  }

  Post *post = [ticket userData];
  
  NSEntityDescription *commentEntity = [Comment entityInManagedObjectContext:context_];

  NSArray *entries = [feed entries];
  for (int i = 0; i < [entries count]; ++i) {
    BuzzEntry *entry = [entries objectAtIndex:i];
    
    Comment *comment = (Comment *)[self objectForValue:[entry identifier]
                                            primaryKey:@"atomID"
                                                entity:commentEntity
                        insertIntoManagedObjectContext:context_];
    if (!comment) {
      continue;
    }

    [context_ obtainPermanentIDsForObjects:[NSArray arrayWithObject:comment] error:&error];

    Author *author = [self authorForPerson:[[entry authors] objectAtIndex:0]];

    [comment setContentHTML:[[entry content] stringValue]];
    [comment setPost:post];
    [comment setAuthor:author];
    [comment setPublished:[[entry publishedDate] date]];
  }

  [post setRepliesLastFetchedDate:[post repliesUpdatedDate]];

  [self save];
}

- (void)ticket:(GDataServiceTicket *)ticket
finishedWithLikersFeed:(PocoFeed *)feed
         error:(NSError *)error {
  [self endSync];
  
  if (error != nil) {
    [delegate_ dao:self networkError:error];
    return;
  }
  
  Post *post = [ticket userData];
  
  NSArray *entries = [feed entries];
  for (int i = 0; i < [entries count]; ++i) {
    PocoEntry *entry = [entries objectAtIndex:i];

    Author *author = [self authorForPocoEntry:entry];

    if (!author) {
      continue;
    }

    [post addLikersObject:author];
  }
  
  
  [self save];
}


- (void)addTicket:(GDataServiceTicket *)ticket
       addedEntry:(BuzzEntry *)entry
            error:(NSError *)error {

  Callback *callback = [ticket userData];

  if (error) {
    [delegate_ dao:self networkError:error];
    [callback callWithObjects:self, error];
    return;
  }

  [self syncConsumption];
  [callback callWithObjects:self, nil];
}


- (NSManagedObject *)objectForValue:(NSString *)value
                         primaryKey:(NSString *)primaryKey
                             entity:(NSEntityDescription *)entity
     insertIntoManagedObjectContext:(NSManagedObjectContext *)context {
  NSPredicate *idPredicate = [NSPredicate predicateWithFormat:@"%K = %@", primaryKey, value];
  
  NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
  [request setEntity:entity];
  [request setPredicate:idPredicate];
  
  NSError *error = nil;
  NSArray *objects = [context_ executeFetchRequest:request error:&error];
  if (error) {
    [delegate_ dao:self storageError:error];
    return nil;
  }
  
  if ([objects count]) {
    return [objects objectAtIndex:0];
  } else {
    NSManagedObject *object = [[[NSManagedObject alloc] initWithEntity:entity
                                        insertIntoManagedObjectContext:context] autorelease];
    if (error) {
      [delegate_ dao:self storageError:error];
      return nil;
    }
    
    [object setValue:value forKey:primaryKey];
    return object;
  }
}


- (Author *)authorForPerson:(GDataPerson *)person {
  Author *author = (Author *)[self objectForValue:[person pocoIdentifier]
                                       primaryKey:@"pocoID"
                                           entity:[Author entityInManagedObjectContext:context_]
                   insertIntoManagedObjectContext:context_];

  [author setName:[person name]];
  [author setProfileURL:[person URI]];
  [author setPhotoURL:[person pocoPhotoURL]];

  return author;
}


- (Author *)authorForPocoEntry:(PocoEntry *)entry {
  Author *author = (Author *)[self objectForValue:[entry identifier]
                                       primaryKey:@"pocoID"
                                           entity:[Author entityInManagedObjectContext:context_]
                   insertIntoManagedObjectContext:context_];
  
  [author setName:[entry displayName]];
  [author setProfileURL:[entry profileURL]];
  
  return author;
}


- (AclEntry *)aclEntryForAclEntryElement:(BuzzAclEntry *)element {
  if ([[element type] isEqualToString:@"group"]) {
    AclGroup *group = (AclGroup *)[self objectForValue:[element pocoIdentifier]
                                            primaryKey:@"pocoID"
                                                entity:[AclGroup entityInManagedObjectContext:context_]
                        insertIntoManagedObjectContext:context_];

    [group setUrl:[element URI]];
    [group setName:[element pocoName]];

    return group;
  } else {
    return nil;
  }
}


- (User *)user {
  NSEntityDescription *userEntity = [User entityInManagedObjectContext:context_];

  NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
  [request setEntity:userEntity];

  NSError *error = nil;
  NSArray *users = [context_ executeFetchRequest:request error:&error];

  if (error) {
    [delegate_ dao:self storageError:error];
    return nil;
  }

  if ([users count]) {
    return [users objectAtIndex:0];
  }

  NSLog(@"making a new user...");
  User *user = [[[User alloc] initWithEntity:userEntity
              insertIntoManagedObjectContext:context_] autorelease];
  [context_ obtainPermanentIDsForObjects:[NSArray arrayWithObject:user] error:&error];
  [self save];

  return user;
}


- (void)reset {
  User *user = [self user];
  [context_ deleteObject:user];
  [self save];
  [self user];
}


- (Post *)newPost {
  return [[Post alloc] initWithEntity:[Post entityInManagedObjectContext:context_]
       insertIntoManagedObjectContext:nil];
}

- (Comment *)newComment {
  return [[Comment alloc] initWithEntity:[Comment entityInManagedObjectContext:context_]
       insertIntoManagedObjectContext:nil];
}


- (void)dealloc {
  [buzzService_ release];
  [store_ release];
  [context_ release];

  [super dealloc];
}

@end
